package com.dbflow5.processor.definition.column;

import com.dbflow5.MapUtils;
import com.dbflow5.StringUtils;
import com.dbflow5.data.Blob;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.TypeName;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

/**
 * Description: Base interface for accessing columns
 *
 * @author Andrew Grosner (fuzz)
 */
public abstract class ColumnAccessor {

    public static CodeBlock modelBlock = CodeBlock.of("model");

    public String propertyName;

    public boolean isPrimitiveTarget = false;

    public ColumnAccessor(String propertyName) {
        this.propertyName = propertyName;
    }

    public abstract CodeBlock get(CodeBlock existingBlock);

    public abstract CodeBlock set(CodeBlock existingBlock, CodeBlock baseVariableName, boolean isDefault);

    protected void prependPropertyName(CodeBlock.Builder code) {
        if(propertyName != null){
            code.add("$L.", propertyName);
        }
    }

    protected void appendPropertyName(CodeBlock.Builder code) {
        if(propertyName != null){
            code.add(".$L", propertyName);
        }
    }

    protected CodeBlock appendAccess(Function<CodeBlock.Builder, Void>codeAccess) {
        CodeBlock.Builder codeBuilder = CodeBlock.builder();
        prependPropertyName(codeBuilder);
        codeAccess.apply(codeBuilder);
        return codeBuilder.build();
    }

    public boolean isPrimitiveTarget() {
        return !this.isPrimitiveTarget;
    }

    public interface GetterSetter {
        String getterName();
        String setterName();
    }

    public static class VisibleScopeColumnAccessor extends ColumnAccessor {
        public VisibleScopeColumnAccessor(String propertyName) {
            super(propertyName);
        }

        @Override
        public CodeBlock set(CodeBlock existingBlock, CodeBlock baseVariableName, boolean isDefault) {
            CodeBlock.Builder codeBlock = CodeBlock.builder();
            if(baseVariableName != null) {
                codeBlock.add("$L.", baseVariableName);
            }
            return codeBlock.add("$L = $L", propertyName, existingBlock).build();
        }

        public CodeBlock get(CodeBlock existingBlock) {
            CodeBlock.Builder codeBlock = CodeBlock.builder();
            if(existingBlock != null){
                codeBlock.add("$L.", existingBlock);
            }
            return codeBlock.add(propertyName).build();
        }
    }

    public static class PrivateScopeColumnAccessor extends ColumnAccessor {

        private String getterName = "";
        private String setterName = "";

        private final boolean useIsForPrivateBooleans;
        private final String optionalGetterParam;

        public PrivateScopeColumnAccessor(String propertyName, GetterSetter getterSetter, boolean useIsForPrivateBooleans, String optionalGetterParam){
            super(propertyName);
            this.useIsForPrivateBooleans = useIsForPrivateBooleans;
            this.optionalGetterParam = optionalGetterParam;

            if(getterSetter != null){
                getterName = getterSetter.getterName();
                setterName = getterSetter.setterName();
            }
        }

        @Override
        public CodeBlock get(CodeBlock existingBlock) {
            CodeBlock.Builder builder = CodeBlock.builder();
            if(existingBlock != null) {
                builder.add(existingBlock + ".");
            }
            builder.add(getterNameElement() + "(" + optionalGetterParam + ")");
            return builder.build();
        }

        @Override
        public CodeBlock set(CodeBlock existingBlock, CodeBlock baseVariableName, boolean isDefault) {
            CodeBlock.Builder builder = CodeBlock.builder();
            if(baseVariableName != null){
                builder.add(baseVariableName + ".");
            }
            builder.add(setterNameElement() + "("+existingBlock+")");
            return builder.build();
        }

        public String getterNameElement() {
            if (StringUtils.isNullOrEmpty(getterName)) {
                if (propertyName != null) {
                    if (useIsForPrivateBooleans && !propertyName.toLowerCase().startsWith("is")) {
                        return "is" + StringUtils.capitalize(propertyName);
                    } else if (!useIsForPrivateBooleans) {
                        return "get" + StringUtils.capitalize(propertyName);
                    } else return propertyName.toLowerCase();
                } else {
                    return "";
                }
            }
            return getterName;
        }

        public String setterNameElement() {
            if (propertyName != null) {
                String setElementName = propertyName;
                if (StringUtils.isNullOrEmpty(setterName)) {
                    if (!setElementName.toLowerCase().startsWith("set")) {
                        if (useIsForPrivateBooleans && setElementName.startsWith("is")) {
                            setElementName = setElementName.replaceFirst("is", "");
                        } else if (useIsForPrivateBooleans && setElementName.startsWith("Is")) {
                            setElementName = setElementName.replaceFirst("Is", "");
                        }
                        return "set" + StringUtils.capitalize(setElementName);
                    } else return "set" + StringUtils.capitalize(setElementName);
                } else return setterName;
            } else return "";
        }
    }

    public static class PackagePrivateScopeColumnAccessor extends ColumnAccessor {

        public ClassName helperClassName;
        public ClassName internalHelperClassName;

        public PackagePrivateScopeColumnAccessor(String propertyName, String packageName, String tableClassName) {
            super(propertyName);
            helperClassName = ClassName.get(packageName, tableClassName + "_" + classSuffix);
            internalHelperClassName = ClassName.get(packageName, tableClassName + "_" + classSuffix);
        }

        @Override
        public CodeBlock get(CodeBlock existingBlock) {
            return CodeBlock.of("$T.get$L($L)", internalHelperClassName,
                    StringUtils.capitalize(propertyName),
                    existingBlock);
        }

        @Override
        public CodeBlock set(CodeBlock existingBlock, CodeBlock baseVariableName, boolean isDefault) {
            return CodeBlock.of("$T.set$L($L, $L)", helperClassName,
                    StringUtils.capitalize(propertyName),
                    baseVariableName,
                    existingBlock);
        }

        public static final String classSuffix = "Helper";

        private static final Map<ClassName, List<String>> methodWrittenMap = new HashMap<>();

        public static boolean containsColumn(ClassName className, String columnName) {
            if(methodWrittenMap.get(className) != null) {
                return methodWrittenMap.get(className).contains(columnName);
            }
            return false;
        }

        /**
         * Ensures we only map and use a package private field generated access method if its necessary.
         *
         * @param className className
         * @param elementName elementName
         */
        public static void putElement(ClassName className, String elementName) {
            List<String> list = MapUtils.getOrPut(methodWrittenMap, className, new ArrayList<>());
            if (!list.contains(elementName)) {
                list.add(elementName);
            }
        }
    }

    public static class TypeConverterScopeColumnAccessor extends ColumnAccessor {
        public String typeConverterFieldName;

        public TypeConverterScopeColumnAccessor(String typeConverterFieldName, String propertyName) {
            super(propertyName);
            this.typeConverterFieldName = typeConverterFieldName;
        }

        @Override
        public CodeBlock get(CodeBlock existingBlock) {
            CodeBlock.Builder codeBlock = CodeBlock.builder();
            codeBlock.add("$L.getDBValue($L", typeConverterFieldName, existingBlock);
            appendPropertyName(codeBlock);
            codeBlock.add(")");
            return codeBlock.build();
        }

        public CodeBlock set(CodeBlock existingBlock, CodeBlock baseVariableName, boolean isDefault) {
            CodeBlock.Builder codeBlock = CodeBlock.builder();
            codeBlock.add("$L.getModelValue($L", typeConverterFieldName, existingBlock);
            appendPropertyName(codeBlock);
            codeBlock.add(")");
            return codeBlock.build();
        }
    }

    public static class EnumColumnAccessor extends ColumnAccessor {
        TypeName propertyTypeName;

        public EnumColumnAccessor(TypeName propertyTypeName, String propertyName) {
            super(propertyName);
            this.propertyTypeName = propertyTypeName;
        }

        @Override
        public CodeBlock get(CodeBlock existingBlock) {
            return appendAccess(builder -> {
                builder.add("$L.name()", existingBlock);
                return null;
            });
        }

        public CodeBlock set(CodeBlock existingBlock, CodeBlock baseVariableName, boolean isDefault) {
            return appendAccess(builder -> {
                if (isDefault) {
                    builder.add(existingBlock);
                } else {
                    builder.add("$T.valueOf($L)", propertyTypeName, existingBlock);
                }
                return null;
            });
        }
    }

    public static class BlobColumnAccessor extends ColumnAccessor {

        public BlobColumnAccessor(String propertyName) {
            super(propertyName);
        }

        @Override
        public CodeBlock get(CodeBlock existingBlock) {
            return appendAccess(builder -> {
                builder.add("$L.getBlob()", existingBlock);
                return null;
            });
        }

        public CodeBlock set(CodeBlock existingBlock, CodeBlock baseVariableName, boolean isDefault) {
            return appendAccess(builder -> {
                if (isDefault) {
                    builder.add(existingBlock);
                } else {
                    builder.add("new $T($L)", ClassName.get(Blob.class), existingBlock);
                }
                return null;
            });
        }
    }

    public static class BooleanColumnAccessor extends ColumnAccessor {

        public BooleanColumnAccessor(String propertyName) {
            super(propertyName);
            isPrimitiveTarget = true;
        }

        @Override
        public CodeBlock get(CodeBlock existingBlock) {
            return appendAccess(builder -> {
                builder.add("$L ? 1 : 0", existingBlock);
                return null;
            });
        }

        @Override
        public CodeBlock set(CodeBlock existingBlock, CodeBlock baseVariableName, boolean isDefault) {
            return appendAccess(builder -> {
                if (isDefault) {
                    builder.add(existingBlock);
                } else {
                    builder.add("$L", existingBlock);
                }
                return null;
            });
        }
    }

    public static class CharColumnAccessor extends ColumnAccessor {

        public CharColumnAccessor(String propertyName) {
            super(propertyName);
            isPrimitiveTarget = true;
        }

        @Override
        public CodeBlock get(CodeBlock existingBlock) {
            return appendAccess(builder -> {
                builder.add("new $T(new char[]{$L})", TypeName.get(String.class), existingBlock);
                return null;
            });
        }

        @Override
        public CodeBlock set(CodeBlock existingBlock, CodeBlock baseVariableName, boolean isDefault) {
            return appendAccess(builder -> {
                if (isDefault) {
                    builder.add(existingBlock);
                } else {
                    builder.add("$L.charAt(0)", existingBlock);
                }
                return null;
            });
        }

    }

    public static class ByteColumnAccessor extends ColumnAccessor {
        public ByteColumnAccessor(String propertyName) {
            super(propertyName);
            isPrimitiveTarget = true;
        }

        public CodeBlock get(CodeBlock existingBlock) {
            return appendAccess(builder -> {
                builder.add("$L", existingBlock);
                return null;
            });
        }

        public CodeBlock set(CodeBlock existingBlock, CodeBlock baseVariableName, boolean isDefault) {
            return appendAccess(builder -> {
                if (isDefault) {
                    builder.add(existingBlock);
                } else {
                    builder.add("($T) $L", TypeName.BYTE, existingBlock);
                }
                return null;
            });
        }
    }
}

